﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;

namespace Mahanet.Abyss.PathCalculation
{
	public class Coordinate2DAimer : IAimer<double, Coordinate2D>
	{
		#region Consts

		private const double DANGER_ZONE_TIME_BUFFER_IN_MS = 50;
		private const double TIME_TO_PASS_DANGER_ZONE_IN_MS = 500;
		private const int NUMBER_OF_INTERSECTION_FIND_ATTEMPTS = 10;
		private const double INTERSECTION_DISTANCE_APPROXIMATION = 0.1;

		#endregion

		#region Fields

		private IGunFacade2D m_GunFacade;

		private HashSet<int> m_IgnoredDataSeries;

		private IClock m_Clock;

		#endregion

		#region C'tor

		public Coordinate2DAimer(IGunFacade2D gunFacade, IClock clock)
		{
			m_GunFacade = gunFacade;

			m_Clock = clock;

			m_IgnoredDataSeries = new HashSet<int>();
		}

		#endregion

		#region Public Properties

		public Segment2D DangerZone { get; set; }

		public Func<double, double> DistanceToTravelTime { get; set; }
		public Func<double, double> DistanceToAngularSkew { get; set; }

		public Coordinate2D GunRotationCenter { get; set; }
		public Vector2D GunZeroAngleDirection { get; set; }
		public double MinimalDataSeriesIntervalInTicks { get; set; }

		#endregion

		#region Public Methods

		public void UpdateApproximator(IPathOracle<double, Coordinate2D> approximator, int dataSeriesId)
		{
			if (m_IgnoredDataSeries.Contains(dataSeriesId))
				return;

			approximator.GenerateApproximation();

			double crossTime;
			if (!CrossesDangerZone(approximator.Approximation, out crossTime))
				return;

			double interceptionTime = crossTime - DANGER_ZONE_TIME_BUFFER_IN_MS;
			Coordinate2D interceptionPoint = approximator.Approximation(interceptionTime);
			double interceptionDistance = interceptionPoint.DistanceTo(GunRotationCenter);

			MoveGunIfNecessary(interceptionPoint, interceptionDistance);

			double travelTime = DistanceToTravelTime(interceptionDistance);
			double fireTime = interceptionTime - travelTime;
			FireIfNecessary(dataSeriesId, fireTime, interceptionPoint);
		}

		#endregion

		#region Private Methods

		private void FireIfNecessary(int dataSeriesId, double fireTime, Coordinate2D interceptionPoint)
		{
			double timeLeftToFire = fireTime - GetCurrentTimeInTicks();

			if (timeLeftToFire < MinimalDataSeriesIntervalInTicks && timeLeftToFire > 0)
			{
				Timer timer = new Timer(m_Clock.ConvertTicksToMs(timeLeftToFire));
				timer.AutoReset = false;
				timer.Elapsed += (sender, e) => FireAtPoint(interceptionPoint, dataSeriesId);
				timer.Start();

				m_IgnoredDataSeries.Add(dataSeriesId);
			}

			if (timeLeftToFire < 0)
			{
				// TODO: handle this better
				m_IgnoredDataSeries.Add(dataSeriesId);

				FireAtPoint(interceptionPoint, dataSeriesId);
			}
		}

		private void MoveGunIfNecessary(Coordinate2D interceptionPoint, double interceptionDistance)
		{
			double angleSkew = DistanceToAngularSkew(interceptionDistance);

			Vector2D fireVector = new Vector2D(GunRotationCenter, interceptionPoint);
			double fireAngle = fireVector.AngleTo(GunZeroAngleDirection);
			double gunAngle = fireAngle + angleSkew;

			m_GunFacade.ChangeGunAngle(gunAngle);
		}

		private bool CrossesDangerZone(FunctionApproximation<double, Coordinate2D> functionApproximation, out double crossTime)
		{
			DataPoint<double, Coordinate2D> startDataPoint, endDataPoint;
			double startSignedDistance, endSignedDistance;
			if (!GetApproximationEdgePointsInDangerZone(functionApproximation, out startDataPoint, out startSignedDistance, out endDataPoint, out endSignedDistance))
			{
				crossTime = double.NaN;
				return false;
			}

			crossTime = FindIntersectionWithDangerZone(functionApproximation, startDataPoint, startSignedDistance, endDataPoint, endSignedDistance);
			var crossPoint = functionApproximation(crossTime);

			return this.DangerZone.IsPointOnSegment(crossPoint);
		}

		private double FindIntersectionWithDangerZone(FunctionApproximation<double, Coordinate2D> functionApproximation, 
													  DataPoint<double, Coordinate2D> pointBeforeIntersection,
													  double startSignedDistance,
													  DataPoint<double, Coordinate2D> pointAfterIntersection,
													  double endSignedDistance)
		{
			if (Math.Abs(startSignedDistance - endSignedDistance) < INTERSECTION_DISTANCE_APPROXIMATION)
			{
				Segment2D functionSegment = new Segment2D(pointBeforeIntersection.Value, pointAfterIntersection.Value);
				Coordinate2D intersection = functionSegment.Line.GetIntersectionPoint(this.DangerZone.Line);
				double intersectionDistance = (intersection - pointBeforeIntersection.Value).Length();
				double totalLength = functionSegment.Length();

				return pointBeforeIntersection.Paramter + (pointAfterIntersection.Paramter - pointBeforeIntersection.Paramter) * intersectionDistance / totalLength;
			}

			// Using halving method
			double newTimeGuess = (pointBeforeIntersection.Paramter + pointAfterIntersection.Paramter) / 2;
			Coordinate2D newPointGuess = functionApproximation(newTimeGuess);
			double newGuessSignedDistance = this.DangerZone.Line.OrientedDistance(newPointGuess);

			var newGuessDataPoint = new DataPoint<double, Coordinate2D>(newTimeGuess, newPointGuess);

			if (newGuessSignedDistance == 0)
			{
				return newGuessDataPoint.Paramter;
			}
			if (newGuessSignedDistance * startSignedDistance < 0)
			{
				return FindIntersectionWithDangerZone(functionApproximation,
													  pointBeforeIntersection,
													  startSignedDistance,
													  newGuessDataPoint,
													  newGuessSignedDistance);
			}
			else
			{
				return FindIntersectionWithDangerZone(functionApproximation,
													  newGuessDataPoint,
													  newGuessSignedDistance,
													  pointAfterIntersection,
													  endSignedDistance);
			}
		}

		private bool GetApproximationEdgePointsInDangerZone(FunctionApproximation<double, Coordinate2D> functionApproximation,
												out DataPoint<double, Coordinate2D> startDataPoint,
												out double startSignedDistance,
												out DataPoint<double, Coordinate2D> endDataPoint,
												out double endSignedDistance)
		{
			
			double startTime = GetCurrentTimeInTicks();
			Coordinate2D startPoint = functionApproximation(startTime);
			startDataPoint = new DataPoint<double, Coordinate2D>(startTime, startPoint);

			startSignedDistance = this.DangerZone.Line.OrientedDistance(startPoint);

			double endTime = startTime;
			Coordinate2D endPoint = startPoint;
			endSignedDistance = startSignedDistance;

			int numberOfAttempts = 0;

			do
			{
				numberOfAttempts++;
				if (numberOfAttempts > NUMBER_OF_INTERSECTION_FIND_ATTEMPTS)
					break;

				endTime += TIME_TO_PASS_DANGER_ZONE_IN_MS;
				endPoint = functionApproximation(endTime);
				endSignedDistance = this.DangerZone.Line.OrientedDistance(endPoint);
			} while (startSignedDistance * endSignedDistance > 0);

			endDataPoint = new DataPoint<double, Coordinate2D>(endTime, endPoint);

			// If we finished without passing the number of attempts, we found what we were looking for
			return numberOfAttempts <= NUMBER_OF_INTERSECTION_FIND_ATTEMPTS;
		}

		private void FireAtPoint(Coordinate2D point, int dataSeriesId)
		{
			m_GunFacade.Fire(point);
		}

		private double GetCurrentTimeInTicks()
		{
			return m_Clock.GetCurrentTime();
		}

		#endregion
	}
}
